from random import Random
from pygo1963.model.Constants import WHITE_COLOR, BLACK_COLOR
from pygo1963.goLogic.GoUtils import get_vertex_around_position
from pygo1963.model.Vertex import Vertex

class AbstractEvaluator:
    '''
    Evaluates the value of a certain board from the white player perspective
    '''

    MAX_EVALUATION = 1000

    def evaluate(self, board):
        """
        Evaluates a position.
        """
        
        raise NotImplementedError
        
class RandomEvaluator(AbstractEvaluator):
    """
    Returns a value without tanking into account the board position.
    """
    
    def __init__(self):
        self.random = Random()
    
    def evaluate(self, board):
        """
        Returns a random value between zero and the max possible evaluation value.
        """

        return Random.randint(self.random, -self.MAX_EVALUATION, self.MAX_EVALUATION)

class SimpleGoEvaluator(AbstractEvaluator):
    """
    Returns a value that only considers capture.
    """
    
    def evaluate(self, board):
        """
        Takes into account:
        * Capture
        """
        
        if board.game_finished:
            if board.winner == WHITE_COLOR:
                return self.MAX_EVALUATION
            else:
                return -self.MAX_EVALUATION
        
        return 0

class AdvacedGoEvaluator(AbstractEvaluator):
    """
    Complex evaluator that takes into account a variety of factors to 
    return a static position evaluation. 
    """
    
    ATARI_VALUE = 100
    FIRST_ORDER_LIBERTY = 3
    GROUP_VALUE = -10
    TERRITORY_VALUE = 2
    
    def __init__(self, evaluate_territory = False):
        """
        Constructor
        """
        
        self.evaluate_territory = evaluate_territory
        self.cache = {}
    
    def evaluate(self, board):
        """
        Takes into account:
        * Capture
        * Number of 1st order liberties.
        * Number of 2th order liberties.
        * Number of 3th order liberties.
        * Connectivity.
        * Territory.
        * Number of stones in atari.
        """
        
        board_string = board.as_string()
        if self.cache.has_key(board_string):
            return self.cache[board_string]
            
        if board.game_finished:
            if board.winner == WHITE_COLOR:
                return self.MAX_EVALUATION
            else:
                return -self.MAX_EVALUATION
        
        liberties_value = self.calculate_liberties_value(board)
        
        if self.evaluate_territory:
            territory_value = self.calculate_territory(board)
        else:
            territory_value = 0
        
        connectivity_value = self.calculate_connectivity(board)
        atari_value = self.calculate_atari_value(board)
        
        final_evaluation = territory_value + connectivity_value + atari_value + liberties_value
        
        self.cache[board_string] = final_evaluation
        
        return final_evaluation
        
    def calculate_liberties_value(self, board):
        """
        Returns a value according to the white and black number of
        liberties. Takes into account first, second and third order liberties.
        """
        
        liberties_value = 0
        
        for group in board.groups:
            if group.color == WHITE_COLOR:
                liberties_value += len(group.liberties) * self.FIRST_ORDER_LIBERTY
            else:
                liberties_value -= len(group.liberties) * self.FIRST_ORDER_LIBERTY
                
        return liberties_value
    
    def calculate_territory(self, board):
        """
        Returns a value for the board position.
        """
        
        territory_value = 0
        
        territory_board = [[0 for _ in range(board.size)] for _ in range(board.size)]           

        for x in range(board.size):
            for y in range(board.size):
                if board.content[x][y] == "W":
                    territory_board[x][y] = 128
                elif board.content[x][y] == "B":
                    territory_board[x][y] = -128
                    
        self.bounzy_algorithm(territory_board)
        
        for x in range(board.size):
            for y in range(board.size):
                if territory_board[x][y] > 0:
                    territory_value += self.TERRITORY_VALUE
                elif territory_board[x][y] < 0:
                    territory_value -= self.TERRITORY_VALUE
        
        return territory_value
    
    def bounzy_algorithm(self, territory_board):
        
        times = 5
        
        for _ in range(times):
            self.dilatate_board(territory_board)
            
        for _ in range(times * (times - 1) + 1):
            self.erase_board(territory_board)
    
    def dilatate_board(self, territory_board):
        """
        Dilatates the territory.
        """
        
        board_size = len(territory_board)

        vertexes_with_value = self.get_vertexes_with_value(territory_board)
        vertexes_to_replace = []

        for vertex in vertexes_with_value:
            near_vertexes = get_vertex_around_position(board_size, vertex)
            
            for near_vertex in near_vertexes:
                
                white_stones_around, black_stones_around = self.count_stones_around(territory_board, near_vertex)
                
                vertex_value = territory_board[near_vertex.x][near_vertex.y]
                
                if vertex_value == 0:                   
                    if white_stones_around != 0 and black_stones_around == 0:
                        vertexes_to_replace.append((near_vertex, white_stones_around))
                    elif black_stones_around != 0 and white_stones_around == 0:
                        vertexes_to_replace.append((near_vertex, -black_stones_around))
                elif vertex_value > 0:
                    if black_stones_around != 0:
                        continue
                    
                    vertexes_to_replace.append((near_vertex, vertex_value + white_stones_around))
                else:
                    if white_stones_around != 0:
                        continue
                    
                    vertexes_to_replace.append((near_vertex, vertex_value - black_stones_around))
                
        self.replace_values(territory_board, vertexes_to_replace)

    def erase_board(self, territory_board):
        """
        Erases the territory.
        """
        
        board_size = len(territory_board)

        vertexes_with_value = self.get_vertexes_with_value(territory_board)
        vertexes_to_replace = []
        
        for vertex in vertexes_with_value:
            near_vertexes = get_vertex_around_position(board_size, vertex)
            value_to_replace = territory_board[vertex.x][vertex.y]
            to_substract = 0
            
            for near_vertex in near_vertexes:
                value = territory_board[near_vertex.x][near_vertex.y]
                
                if (value > 0 and value_to_replace < 0) or value == 0:#Fix!
                    to_substract += 1
            
            if value_to_replace > 0:
                if value_to_replace < to_substract:
                    vertexes_to_replace.append((vertex, 0))
                else:
                    vertexes_to_replace.append((vertex, value_to_replace - to_substract))
            else:
                if abs(value_to_replace) < to_substract:
                    vertexes_to_replace.append((vertex, 0))
                else:
                    vertexes_to_replace.append((vertex, value_to_replace + to_substract))
        
        self.replace_values(territory_board, vertexes_to_replace)
    
    def count_stones_around(self, territory_board, vertex):
        """
        Returns the number of white and black stones of stones
        around a vertex. 
        """
        
        vertexes_to_check = get_vertex_around_position(len(territory_board), vertex)
                
        black_stones_around = 0
        white_stones_around = 0
                   
        for vertex_to_check in vertexes_to_check:
            if territory_board[vertex_to_check.x][vertex_to_check.y] > 0:
                white_stones_around += 1
            elif territory_board[vertex_to_check.x][vertex_to_check.y] < 0:
                black_stones_around += 1
                
        return white_stones_around, black_stones_around
    
    def replace_values(self, territory_board, vertexes_to_replace):
        """
        Replace the values of the territory board with the new ones.
        """
        
        for vertex_to_replace, value in vertexes_to_replace:
            territory_board[vertex_to_replace.x][vertex_to_replace.y] = value
    
    def get_vertexes_with_value(self, territory_board):
        """
        Returns the vertexes of the matrix whose value is different from zero.
        """
        
        board_size = len(territory_board)
        
        return [Vertex(x, y) for x in range(board_size) 
                             for y in range(board_size) 
                                  if territory_board[x][y] != 0]
        
    def calculate_connectivity(self, board):
        """
        Returns a value for the board connectivity.
        """

        connectivity_value = 0
        
        connectivity_value += self.GROUP_VALUE * len(board.get_groups_by_color(WHITE_COLOR))
        connectivity_value -= self.GROUP_VALUE * len(board.get_groups_by_color(BLACK_COLOR))
        
        return connectivity_value
    
    def calculate_atari_value(self, board):
        """
        Returns a value according to the number of atari positions on
        the board.
        """
        
        atari_value = 0
        
        for group in board.groups:
            if group.is_atari():
                if group.color == WHITE_COLOR:
                    atari_value -= self.ATARI_VALUE
                else:
                    atari_value += self.ATARI_VALUE

        return atari_value